[Previous][Up][Next] (#fcl-res)

How to implement a new resource reader

Remark: This chapter assumes you have some experience in using this library.

We'll see how to implement a reader for a new resource file format. A resource reader is a descendant of TAbstractResourceReader, and it's usually implemented in a unit named namereader, where name is file format name.

Suppose we must write a reader for file format foo; we could start with a unit like this:

unit fooreader;

{$MODE OBJFPC} {$H+}

interface

uses
  Classes, SysUtils, resource;

type
  TFooResourceReader = class(TAbstractResourceReader)
  protected
    function GetExtensions : string; override;
    function GetDescription : string; override;
    procedure Load(aResources : TResources; aStream : TStream); override;
    function CheckMagic(aStream : TStream) : boolean; override;
  public
    constructor Create; override;
  end;

implementation

function TFooResourceReader.GetExtensions: string;
begin

end;

function TFooResourceReader.GetDescription: string;
begin

end;

procedure TFooResourceReader.Load(aResources: TResources; aStream: TStream);
begin

end;

function TFooResourceReader.CheckMagic(aStream: TStream): boolean;
begin

end;

constructor TFooResourceReader.Create;
begin

end;

initialization
  TResources.RegisterReader('.foo',TFooResourceReader);

end.

Note that in the initialization section, TFooResourceReader is registered for extension .foo.

We must implement abstract methods of TAbstractResourceReader. Let's start with the simpler ones, GetExtensions and GetDescription.

function TFooResourceReader.GetExtensions: string;
begin
  Result:='.foo';
end;

function TFooResourceReader.GetDescription: string;
begin
  Result:='FOO resource reader';
end;

Now let's see CheckMagic method. This method is called with a stream as a parameter, and the reader must return true if it recognizes the stream as a valid one. Usually this means checking some magic number or header.

function TFooResourceReader.CheckMagic(aStream: TStream): boolean;
var signature : array[0..3] of char;
begin
  Result:=false;
  try
    aStream.ReadBuffer(signature[0],4);
  except
    on e : EReadError do exit;
  end;
  Result:=signature='FOO*';
end; 

Suppose our foo files start with a 4-byte signature 'FOO*'. This method checks the signature and returns true if it is verified. Note that it catches EReadError exception raised by TStream: this way, if the stream is too short it returns false (as it should, since magic is not valid) instead of letting the exception to propagate.

Now let's see Load. This method must read the stream and create resources in the TResources object, with information about their name, type, position and size of their raw data, and so on.

procedure TFooResourceReader.Load(aResources: TResources; aStream: TStream);
begin
  if not CheckMagic(aStream) then
    raise EResourceReaderWrongFormatException.Create('');
  try
    ReadResources(aResources,aStream);
  except
    on e : EReadError do
      raise EResourceReaderUnexpectedEndOfStreamException.Create('');
  end;
end;

First of all, this method checks file magic number, calling CheckMagic method we already implemented. This is necessary since CheckMagic is not called before Load: CheckMagic is invoked by TResources when probing a stream, while Load is invoked when loading resources (so if the user passed a reader object to a TResources object, CheckMagic is never called). Note also that the stream is always at its starting position when these methods are called.

If magic number is ok, our method invokes another method to do the actual loading. If during this process the stream can't be read, an EResourceReaderUnexpectedEndOfStreamException exception is raised.

So, let's implement the private method which will load resources.

Suppose that our foo format is very simple:

To start with, our method will be:

procedure TFooResourceReader.ReadResources(aResources: TResources; aStream: TStream);
var Count, i: longword;
    aType, aName, aLangID : longword;
    aDataSize : longword;
begin
  //read remaining file header
  aStream.ReadBuffer(Count,sizeof(Count));
  aStream.Seek(8,soFromCurrent);

  for i:=1 to Count do
  begin
    //read resource header
    aStream.ReadBuffer(aType,sizeof(aType));
    aStream.ReadBuffer(aName,sizeof(aName));
    aStream.ReadBuffer(aLangID,sizeof(aLangID));
    aStream.ReadBuffer(aDataSize,sizeof(aDataSize));

  end;
end;

Since in Load we called CheckMagic, which read the first 4 bytes of the header, we must read the remaining 12: we read the number of resources, and we skip the other 8 bytes of padding.

Then, for each resource, we read the resource header. Note that if we are running on a big endian system we should swap the bytes we read, e.g. calling SwapEndian function, but for simplicity this is omitted.

Now, we should create a resource. Of which class? Well, we must use resfactory unit. In fact it contains TResourceFactory class, which is an expert in creating resources of the right class: when the user adds a unit containing a resource class to the uses clause of its program, the resource class registers itself with TResourceFactory. This way it knows how to map resource types to resource classes.

We need to have type and name of the resource to create as TResourceDesc objects: instead of creating and destroying these objects for each resource, we'll create a couple in the creator of our reader and we'll destroy them in the destructor, so that they will live for the whole life of our reader. Let's name them workType and workName.

Our code becomes:

uses
  resfactory;

procedure TFooResourceReader.ReadResources(aResources: TResources; aStream: TStream);
var Count, i: longword;
    aType, aName, aLangID : longword;
    aDataSize : longword;
    aRes : TAbstractResource;
begin
  //read remaining file header
  aStream.ReadBuffer(Count,sizeof(Count));
  aStream.Seek(8,soFromCurrent);

  for i:=1 to Count do
  begin
    //read resource header
    aStream.ReadBuffer(aType,sizeof(aType));
    aStream.ReadBuffer(aName,sizeof(aName));
    aStream.ReadBuffer(aLangID,sizeof(aLangID));
    aStream.ReadBuffer(aDataSize,sizeof(aDataSize));

    //create the resource
    workType.ID:=aType;
    workName.ID:=aName;
    aRes:=TResourceFactory.CreateResource(workType,workName);
    SetDataSize(aRes,aDataSize);
    SetDataOffset(aRes,aStream.Position);
    aRes.LangID:=aLangID;

  end;
end;

Note that after the resource has been created we set its data size and data offset. Data offset is the current position in the stream, since in our FOO file format resource data immediately follows resource header.

What else do we need to do? Of course we must create RawData stream for our resource, so that raw data can be accessed with the caching mechanism. We will create a TResourceDataStream object, telling it which resource and stream it is associated to, which its size will be and which class its underlying cached stream must be created from.

So we add resdatastream to the uses clause, declare another local variable

aRawData : TResourceDataStream;

and add these lines in the for loop

aRawData:=TResourceDataStream.Create(aStream,aRes,aRes.DataSize,TCachedResourceDataStream);
SetRawData(aRes,aRawData);

That is, aRawData will create its underlying stream as a TCachedResourceDataStream over the portion of aStream that starts at current position and ends after aRes.DataSize bytes.

We almost finished: now we must add the newly created resource to the TResources object and move stream position to the next resource header. Complete code for ReadResources method is:

procedure TFooResourceReader.ReadResources(aResources: TResources; aStream: TStream);
var Count, i: longword;
    aType, aName, aLangID : longword;
    aDataSize : longword;
    aRes : TAbstractResource;
    aRawData : TResourceDataStream;
begin
  //read remaining file header
  aStream.ReadBuffer(Count,sizeof(Count));
  aStream.Seek(8,soFromCurrent);

  for i:=1 to Count do
  begin
    //read resource header
    aStream.ReadBuffer(aType,sizeof(aType));
    aStream.ReadBuffer(aName,sizeof(aName));
    aStream.ReadBuffer(aLangID,sizeof(aLangID));
    aStream.ReadBuffer(aDataSize,sizeof(aDataSize));

    //create the resource
    workType.ID:=aType;
    workName.ID:=aName;
    aRes:=TResourceFactory.CreateResource(workType,workName);
    SetDataSize(aRes,aDataSize);
    SetDataOffset(aRes,aStream.Position);
    aRes.LangID:=aLangID;

    //set raw data
    aRawData:=TResourceDataStream.Create(aStream,aRes,aRes.DataSize,TCachedResourceDataStream);
    SetRawData(aRes,aRawData);

    //add to aResources
    try
      aResources.Add(aRes);
    except
      on e : EResourceDuplicateException do
      begin
        aRes.Free;
        raise;
      end;
    end;

    //go to next resource header
    aStream.Seek(aDataSize,soFromCurrent);
    Align4Bytes(aStream);
  end;
end;

Align4Bytes is a private method (not shown for simplicity) that sets stream position to the next multiple of 4 if needed, since FOO file format specifies that resource data must be padded to end on a 4 byte boundary.

Note: We have used Add method to populate the TResources object. More complex file formats store resources in a tree hierarchy; since TResources internally stores resources in this way too, a reader can choose to acquire a reference to the internal tree used by the TResources object (see TAbstractResourceReader.GetTree), populate it and notify the TResources object about the added resources (see TAbstractResourceReader.AddNoTree). For these file formats resources can be loaded faster, since there is no overhead involved in keeping a separate resource tree in the reader.

That's all. Now you should be able to create a real resource reader.


Documentation generated on: Oct 30 2020